﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Globalization;
using System.Collections;
using System.Text.RegularExpressions;

namespace Dapper.Core
{
    public enum JObjectKind
    {
        Object,
        Array,
        String,
        Number,
        Boolean,
        Null
    }

    public enum IntegerSize
    {
        UInt64,
        Int64,
        UInt32,
        Int32,
        UInt16,
        Int16,
        UInt8,
        Int8,
    }

    public enum FloatSize
    {
        Double,
        Single
    }

    public class JObject
    {
        public JObjectKind Kind { get; private set; }

        public Dictionary<string, JObject> ObjectValue { get; private set; }
        public List<JObject> ArrayValue { get; private set; }
        public string StringValue { get; private set; }
        public bool BooleanValue { get; private set; }

        public int Count
        {
            get
            {
                return Kind == JObjectKind.Array ? ArrayValue.Count
                     : Kind == JObjectKind.Object ? ObjectValue.Count
                     : 0;
            }
        }

        public double DoubleValue { get; private set; }
        public float FloatValue { get; private set; }
        public ulong ULongValue { get; private set; }
        public long LongValue { get; private set; }
        public uint UIntValue { get; private set; }
        public int IntValue { get; private set; }
        public ushort UShortValue { get; private set; }
        public short ShortValue { get; private set; }
        public byte ByteValue { get; private set; }
        public sbyte SByteValue { get; private set; }

        public bool IsNegative { get; private set; }
        public bool IsFractional { get; private set; }
        public IntegerSize MinInteger { get; private set; }
        public FloatSize MinFloat { get; private set; }

        public JObject this[string key] { get { return ObjectValue[key]; } }
        public JObject this[int key] { get { return ArrayValue[key]; } }

        public static explicit operator string(JObject obj) { return obj.StringValue; }
        public static explicit operator bool(JObject obj) { return obj.BooleanValue; }
        public static explicit operator double(JObject obj) { return obj.DoubleValue; }
        public static explicit operator float(JObject obj) { return obj.FloatValue; }
        public static explicit operator ulong(JObject obj) { return obj.ULongValue; }
        public static explicit operator long(JObject obj) { return obj.LongValue; }
        public static explicit operator uint(JObject obj) { return obj.UIntValue; }
        public static explicit operator int(JObject obj) { return obj.IntValue; }
        public static explicit operator ushort(JObject obj) { return obj.UShortValue; }
        public static explicit operator short(JObject obj) { return obj.ShortValue; }
        public static explicit operator byte(JObject obj) { return obj.ByteValue; }
        public static explicit operator sbyte(JObject obj) { return obj.SByteValue; }

        public static JObject CreateString(string str) { return new JObject(str); }
        public static JObject CreateBoolean(bool b) { return new JObject(b); }
        public static JObject CreateNull() { return new JObject(); }
        public static JObject CreateNumber(string integer, string frac, string exp) { return new JObject(integer, frac, exp); }
        public static JObject CreateArray(List<JObject> list) { return new JObject(list); }
        public static JObject CreateObject(Dictionary<string, JObject> dict) { return new JObject(dict); }

        private JObject(string str)
        {
            Kind = JObjectKind.String;
            StringValue = str;
        }

        private JObject(bool b)
        {
            Kind = JObjectKind.Boolean;
            BooleanValue = b;
        }

        private JObject()
        {
            Kind = JObjectKind.Null;
        }

        private JObject(string integer, string frac, string exp)
        {
            Kind = JObjectKind.Number;
            if (frac == "" && exp == "")
            {
                MakeInteger(integer);
            }
            else
            {
                MakeFloat(integer, frac, exp);
            }
        }

        private JObject(List<JObject> list)
        {
            Kind = JObjectKind.Array;
            ArrayValue = list;
        }

        private JObject(Dictionary<string, JObject> dict)
        {
            Kind = JObjectKind.Object;
            ObjectValue = dict;
        }

        private void MakeInteger(string integer)
        {
            if (integer[0] == '-')
            {
                LongValue = Int64.Parse(integer, CultureInfo.InvariantCulture);
                IsNegative = true;
                MinInteger = IntegerSize.Int64;

                if (LongValue >= Int32.MinValue)
                {
                    IntValue = (int)LongValue;
                    MinInteger = IntegerSize.Int32;
                }

                if (LongValue >= Int16.MinValue)
                {
                    ShortValue = (short)LongValue;
                    MinInteger = IntegerSize.Int16;
                }

                if (LongValue >= SByte.MinValue)
                {
                    SByteValue = (sbyte)LongValue;
                    MinInteger = IntegerSize.Int8;
                }

                DoubleValue = LongValue;
                MinFloat = FloatSize.Double;

                if (DoubleValue >= -Single.MaxValue)
                {
                    FloatValue = (float)DoubleValue;
                    MinFloat = FloatSize.Single;
                }
            }
            else
            {
                ULongValue = UInt64.Parse(integer, CultureInfo.InvariantCulture);
                IsNegative = false;
                MinInteger = IntegerSize.UInt64;

                if (ULongValue <= Int64.MaxValue)
                {
                    LongValue = (long)ULongValue;
                    MinInteger = IntegerSize.Int64;
                }

                if (ULongValue <= UInt32.MaxValue)
                {
                    UIntValue = (uint)ULongValue;
                    MinInteger = IntegerSize.UInt32;
                }

                if (ULongValue <= Int32.MaxValue)
                {
                    IntValue = (int)ULongValue;
                    MinInteger = IntegerSize.Int32;
                }

                if (ULongValue <= UInt16.MaxValue)
                {
                    UShortValue = (ushort)ULongValue;
                    MinInteger = IntegerSize.UInt16;
                }

                if (ULongValue <= (ulong)Int16.MaxValue)
                {
                    ShortValue = (short)ULongValue;
                    MinInteger = IntegerSize.Int16;
                }

                if (ULongValue <= Byte.MaxValue)
                {
                    ByteValue = (byte)ULongValue;
                    MinInteger = IntegerSize.UInt8;
                }

                if (ULongValue <= (ulong)SByte.MaxValue)
                {
                    SByteValue = (sbyte)ULongValue;
                    MinInteger = IntegerSize.Int8;
                }

                DoubleValue = ULongValue;
                MinFloat = FloatSize.Double;

                if (DoubleValue <= Single.MaxValue)
                {
                    FloatValue = (float)DoubleValue;
                    MinFloat = FloatSize.Single;
                }
            }
        }

        private void MakeFloat(string integer, string frac, string exp)
        {
            DoubleValue = Double.Parse(integer + frac + exp, CultureInfo.InvariantCulture);
            MinFloat = FloatSize.Double;
            IsFractional = true;

            if (DoubleValue < 0)
            {
                IsNegative = true;

                if (DoubleValue >= -Single.MaxValue)
                {
                    FloatValue = (float)DoubleValue;
                    MinFloat = FloatSize.Single;
                }
            }
            else if (DoubleValue <= Single.MaxValue)
            {
                FloatValue = (float)DoubleValue;
                MinFloat = FloatSize.Single;
            }
        }
    }
    public class Json
    {
        public static string Encode(object obj)
        {
            var encoder = new Json();
            encoder.EncodeObject(obj);
            return encoder._buffer.ToString();
        }

        private readonly StringBuilder _buffer = new StringBuilder();

        private static readonly Dictionary<char, string> EscapeChars1 =
            new Dictionary<char, string>
                {
                    { '"', "\\\"" },
                    { '\\', "\\\\" },
                    { '\b', "\\b" },
                    { '\f', "\\f" },
                    { '\n', "\\n" },
                    { '\r', "\\r" },
                    { '\t', "\\t" },
                    { '\u2028', "\\u2028" },
                    { '\u2029', "\\u2029" }
                };

        private Json() { }

        private void EncodeObject(object obj)
        {
            if (obj == null)
            {
                EncodeNull();
            }
            else if (obj is string)
            {
                EncodeString((string)obj);
            }
            else if (obj is float)
            {
                EncodeFloat((float)obj);
            }
            else if (obj is double)
            {
                EncodeDouble((double)obj);
            }
            else if (obj is int)
            {
                EncodeLong((int)obj);
            }
            else if (obj is uint)
            {
                EncodeULong((uint)obj);
            }
            else if (obj is long)
            {
                EncodeLong((long)obj);
            }
            else if (obj is ulong)
            {
                EncodeULong((ulong)obj);
            }
            else if (obj is short)
            {
                EncodeLong((short)obj);
            }
            else if (obj is ushort)
            {
                EncodeULong((ushort)obj);
            }
            else if (obj is byte)
            {
                EncodeULong((byte)obj);
            }
            else if (obj is bool)
            {
                EncodeBool((bool)obj);
            }
            else if (obj is IDictionary)
            {
                EncodeDictionary((IDictionary)obj);
            }
            else if (obj is IEnumerable)
            {
                EncodeEnumerable((IEnumerable)obj);
            }
            else if (obj is Enum)
            {
                EncodeObject(Convert.ChangeType(obj, Enum.GetUnderlyingType(obj.GetType())));
            }
            else
            {
                throw new ArgumentException("Can't serialize object of type " + obj.GetType().Name, "obj");
            }
        }

        private void EncodeNull()
        {
            _buffer.Append("null");
        }

        private void EncodeString(string str)
        {
            _buffer.Append('"');
            foreach (var c in str)
            {
                if (EscapeChars1.ContainsKey(c))
                {
                    _buffer.Append(EscapeChars1[c]);
                }
                else
                {
                    if (c > 0x80 || c < 0x20)
                    {
                        _buffer.Append("\\u" + Convert.ToString(c, 16)
                                                   .ToUpper(CultureInfo.InvariantCulture)
                                                   .PadLeft(4, '0'));
                    }
                    else
                    {
                        _buffer.Append(c);
                    }
                }
            }
            _buffer.Append('"');
        }

        private void EncodeFloat(float f)
        {
            _buffer.Append(f.ToString(CultureInfo.InvariantCulture));
        }

        private void EncodeDouble(double d)
        {
            _buffer.Append(d.ToString(CultureInfo.InvariantCulture));
        }

        private void EncodeLong(long l)
        {
            _buffer.Append(l.ToString(CultureInfo.InvariantCulture));
        }

        private void EncodeULong(ulong l)
        {
            _buffer.Append(l.ToString(CultureInfo.InvariantCulture));
        }

        private void EncodeBool(bool b)
        {
            _buffer.Append(b ? "true" : "false");
        }

        private void EncodeDictionary(IDictionary d)
        {
            var isFirst = true;
            _buffer.Append('{');
            foreach (DictionaryEntry pair in d)
            {
                if (!(pair.Key is string))
                {
                    throw new ArgumentException("Dictionary keys must be strings", "d");
                }
                if (!isFirst) _buffer.Append(',');
                EncodeString((string)pair.Key);
                _buffer.Append(':');
                EncodeObject(pair.Value);
                isFirst = false;
            }
            _buffer.Append('}');
        }

        private void EncodeEnumerable(IEnumerable e)
        {
            var isFirst = true;
            _buffer.Append('[');
            foreach (var obj in e)
            {
                if (!isFirst) _buffer.Append(',');
                EncodeObject(obj);
                isFirst = false;
            }
            _buffer.Append(']');
        }

         private const char ObjectStart = '{';
        private const char ObjectEnd = '}';
        private const char ObjectPairSeparator = ',';
        private const char ObjectSeparator = ':';
        private const char ArrayStart = '[';
        private const char ArrayEnd = ']';
        private const char ArraySeparator = ',';
        private const char StringStart = '"';
        private const char NullStart = 'n';
        private const char TrueStart = 't';
        private const char FalseStart = 'f';

        private static readonly Regex NumberRegex = new Regex("(-?(?:0|[1-9]\\d*))(\\.\\d+)?([eE][-+]?\\d+)?",
                                                              RegexOptions.CultureInvariant | RegexOptions.Multiline);

        public static JObject Decode(string json)
        {
            var data = Scan(json, 0);
            return data.Result;
        }

        private struct ScannerData
        {
            public readonly JObject Result;
            public readonly int Index;

            public ScannerData(JObject result, int index)
            {
                Result = result;
                Index = index;
            }
        }

        private static readonly Dictionary<char, string> EscapeChars2 =
            new Dictionary<char, string>
                {
                    { '"', "\"" },
                    { '\\', "\\" },
                    { 'b', "\b" },
                    { 'f', "\f" },
                    { 'n', "\n" },
                    { 'r', "\r" },
                    { 't', "\t" },
                };

        private static ScannerData Scan(string json, int index)
        {
            index = SkipWhitespace(json, index);
            var nextChar = json[index];

            switch (nextChar)
            {
                case StringStart:
                    return ScanString(json, index + 1);
                case TrueStart:
                    return ScanTrue(json, index);
                case FalseStart:
                    return ScanFalse(json, index);
                case NullStart:
                    return ScanNull(json, index);
                case ArrayStart:
                    return ScanArray(json, index);
                case ObjectStart:
                    return ScanObject(json, index);
                default:
                    if (IsNumberStart(nextChar))
                    {
                        return ScanNumber(json, index);
                    }
                    throw new ParseError("Unexpected token " + nextChar, index);
            }
        }

        private static ScannerData ScanString(string json, int index)
        {
            var strBuilder = new StringBuilder();
            while (json[index] != '"')
            {
                if (json[index] == '\\')
                {
                    ++index;
                    if (EscapeChars2.ContainsKey(json[index]))
                    {
                        strBuilder.Append(EscapeChars2[json[index]]);
                        ++index;
                    }
                    else if (json[index] == 'u')
                    {
                        ++index;
                        var unicodeSequence = Convert.ToInt32(json.Substring(index, 4), 16);
                        strBuilder.Append((char)unicodeSequence);
                        index += 4;
                    }
                }
                else
                {
                    strBuilder.Append(json[index]);
                    ++index;
                }
            }
            return new ScannerData(JObject.CreateString(strBuilder.ToString()), index + 1);
        }

        private static ScannerData ScanTrue(string json, int index)
        {
            return new ScannerData(JObject.CreateBoolean(true), ExpectConstant(json, index, "true"));
        }

        private static ScannerData ScanFalse(string json, int index)
        {
            return new ScannerData(JObject.CreateBoolean(false), ExpectConstant(json, index, "false"));
        }

        private static ScannerData ScanNull(string json, int index)
        {
            return new ScannerData(JObject.CreateNull(), ExpectConstant(json, index, "null"));
        }

        private static ScannerData ScanNumber(string json, int index)
        {
            var match = NumberRegex.Match(json, index);

            if (!match.Success)
            {
                throw new ParseError("Could not parse number", index);
            }

            return new ScannerData(JObject.CreateNumber(match.Groups[1].Value,
                                                        match.Groups[2].Value,
                                                        match.Groups[3].Value), index + match.Groups[0].Length);
        }

        private static ScannerData ScanArray(string json, int index)
        {
            var list = new List<JObject>();

            var nextTokenIndex = SkipWhitespace(json, index + 1);
            if (json[nextTokenIndex] == ArrayEnd) return new ScannerData(JObject.CreateArray(list), nextTokenIndex + 1);

            while (json[index] != ArrayEnd)
            {
                ++index;
                var result = Scan(json, index);
                index = SkipWhitespace(json, result.Index);
                if (json[index] != ArraySeparator && json[index] != ArrayEnd)
                {
                    throw new ParseError("Expecting array separator (,) or array end (])", index);
                }
                list.Add(result.Result);
            }
            return new ScannerData(JObject.CreateArray(list), index + 1);
        }

        private static ScannerData ScanObject(string json, int index)
        {
            var dict = new Dictionary<string, JObject>();

            var nextTokenIndex = SkipWhitespace(json, index + 1);
            if (json[nextTokenIndex] == ObjectEnd) return new ScannerData(JObject.CreateObject(dict), nextTokenIndex + 1);

            while (json[index] != ObjectEnd)
            {
                ++index;
                var keyResult = Scan(json, index);
                if (keyResult.Result.Kind != JObjectKind.String)
                {
                    throw new ParseError("Object keys must be strings", index);
                }
                index = SkipWhitespace(json, keyResult.Index);
                if (json[index] != ObjectSeparator)
                {
                    throw new ParseError("Expecting object separator (:)", index);
                }
                ++index;
                var valueResult = Scan(json, index);
                index = SkipWhitespace(json, valueResult.Index);
                if (json[index] != ObjectEnd && json[index] != ObjectPairSeparator)
                {
                    throw new ParseError("Expecting object pair separator (,) or object end (})", index);
                }
                dict[keyResult.Result.StringValue] = valueResult.Result;
            }
            return new ScannerData(JObject.CreateObject(dict), index + 1);
        }

        private static int SkipWhitespace(string json, int index)
        {
            while (char.IsWhiteSpace(json[index]))
            {
                ++index;
            }
            return index;
        }

        private static int ExpectConstant(string json, int index, string expected)
        {
            if (json.Substring(index, expected.Length) != expected)
            {
                throw new ParseError(string.Format("Expected '{0}' got '{1}'",
                                                   expected,
                                                   json.Substring(index, expected.Length)),
                                     index);
            }
            return index + expected.Length;
        }

        private static bool IsNumberStart(char b)
        {
            return b == '-' || (b >= '0' && b <= '9');
        }
    }

        public class ParseError : Exception
    {
        public readonly int Position;

        public ParseError(string message, int position)
            : base(message)
        {
            Position = position;
        }
    }

}
